/*
 * Reksio - Memory Map Editor
 * Copyright (C) 2023 CERN
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * In applying this licence, CERN does not waive the privileges and immunities
 * granted to it by virtue of its status as an Intergovernmental Organization or
 * submit itself to any jurisdiction.
 */

#include "customnodesview.h"
#include "commands.h"
#include "validatornode.h"
#include "attributecontainervalidator.h"
#include "utils.h"
#include "pythonaction.h"

#include <QHeaderView>
#include <QDropEvent>
#include <QKeyEvent>
#include <QDebug>
#include <QClipboard>
#include <QApplication>
CustomNodesView::CustomNodesView(QWidget *parent):
    QTreeView (parent)
{
    // context menu
    treeContextMenu = new QMenu(this);
    // add child
    add_submenu = treeContextMenu->addMenu(QIcon(":/icons/icons/resources/16x16/add.png"), "Add...");
    add_child_submenu = add_submenu->addMenu("Child...");
    add_attribute_submenu = add_submenu->addMenu("Attribute...");
    // insert sibling
    insert_before_sibling_submenu = treeContextMenu->addMenu(QIcon(":/icons/icons/resources/16x16/add.png"), "Insert before...");
    insert_after_sibling_submenu = treeContextMenu->addMenu(QIcon(":/icons/icons/resources/16x16/add.png"), "Insert after...");
    // remove node
    QAction * remove_node = new QAction(QIcon(":/icons/icons/resources/16x16/delete.png"), "Remove node", treeContextMenu);
    connect(remove_node, &QAction::triggered, this, &CustomNodesView::removeNodes);
    treeContextMenu->addAction(remove_node);
    remove_attribute_submenu = treeContextMenu->addMenu(QIcon(":/icons/icons/resources/16x16/delete.png"), "Remove attribute...");
    // duplicate node
    QAction * duplicate_node = new QAction(QIcon(":/icons/icons/resources/16x16/node_duplicate.png"), "Duplicate node", treeContextMenu);
    connect(duplicate_node, &QAction::triggered, this, &CustomNodesView::duplicateNodes);
    treeContextMenu->addAction(duplicate_node);
    // move up
    QAction * move_up = new QAction(QIcon(":/icons/icons/resources/16x16/move_child_up.png"), "Move up", treeContextMenu);
    connect(move_up, &QAction::triggered, this, &CustomNodesView::moveUpNode);
    treeContextMenu->addAction(move_up);
    // move down
    QAction * move_down = new QAction(QIcon(":/icons/icons/resources/16x16/move_child_down.png"), "Move down", treeContextMenu);
    connect(move_down, &QAction::triggered, this, &CustomNodesView::moveDownNode);
    treeContextMenu->addAction(move_down);

    python_actions_submenu = treeContextMenu->addMenu("Custom actions...");
    // cut
//    QAction* cut = new QAction("Cut");
//    connect(cut, &QAction::triggered, this, [this](){this->cut({currentIndex()});});
//    treeContextMenu->addAction(cut);
//    // copy
//    QAction* copy = new QAction("Copy");
//    connect(copy, &QAction::triggered, this, [this](){this->copy({currentIndex()});});
//    treeContextMenu->addAction(copy);
//    // paste
//    QAction* paste = new QAction("Paste");
//    connect(paste, &QAction::triggered, this, [this](){this->paste({currentIndex()});});
//    treeContextMenu->addAction(paste);

    setContextMenuPolicy(Qt::CustomContextMenu);

    connect(this, &CustomNodesView::customContextMenuRequested, this, &CustomNodesView::onCustomContextMenu);

    // tree view
    setDragEnabled(true);
    setAcceptDrops(true);
    setDropIndicatorShown(true);
    header()->setStretchLastSection(true);
    connect(this, &CustomNodesView::expanded, this, [this](const QModelIndex&){resizeColumnToContents(0);});
    setSelectionMode(QAbstractItemView::ContiguousSelection);
    setSelectionBehavior(QAbstractItemView::SelectRows);
    setDragDropMode(QAbstractItemView::InternalMove);

    setAutoExpandDelay(500);

    setAnimated(true);

    setAlternatingRowColors(true);
}

void CustomNodesView::dropEvent(QDropEvent *event)
{
    QTreeView::dropEvent(event);
//    const QModelIndex& index = indexAt(event->pos());
//    expand(index);
}

void CustomNodesView::dragMoveEvent(QDragMoveEvent *event)
{
    QTreeView::dragMoveEvent(event);
//        const QModelIndex& index = indexAt(event->pos());

    //        expand(index);
}

void CustomNodesView::keyPressEvent(QKeyEvent *event)
{
    if(event->type() == QKeyEvent::KeyPress)
    {
        if(event->matches(QKeySequence::Copy))
        {
            copy(selectionModel()->selectedIndexes());
        }
        else if (event->matches(QKeySequence::Paste))
        {
            paste(currentIndex());
        }
        else if (event->matches(QKeySequence::Cut))
        {
            cut(selectionModel()->selectedIndexes());
        }
        else if(event->matches(QKeySequence::Delete))
        {
            removeNodes();
        }
        else
        {
            QTreeView::keyPressEvent(event);
        }
    }
    else
    {
        QTreeView::keyPressEvent(event);
    }
}

void CustomNodesView::copy(const QModelIndexList &indexes)
{
    copyOrCut(indexes, Qt::CopyAction);
}

void CustomNodesView::cut(const QModelIndexList &indexes)
{
    copyOrCut(indexes, Qt::MoveAction);
}

void CustomNodesView::paste(const QModelIndex &index)
{
    if(!getNodesModel()->isEditable(index))
        return;
    QClipboard* clipboard = QApplication::clipboard();
    const QMimeData* data = clipboard->mimeData();
    const bool result = model()->dropMimeData(data, Qt::MoveAction, index.row(), index.column(), index.parent());
    if(!result)
    {
        // lets try to copy it as a children
        model()->dropMimeData(data, Qt::MoveAction, model()->rowCount(index), index.column(), index);
    }
}

NodesModel *CustomNodesView::getNodesModel()
{
    return static_cast<NodesModel*>(model());
}

void CustomNodesView::setUndoStack(QUndoStack *stack)
{
    undoStack = stack;
}

QUndoStack *CustomNodesView::getUndoStack()
{
    return undoStack;
}

AttributesModel *CustomNodesView::getAttributesModel(const QModelIndex &nodesIndex)
{
    return getNodesModel()->getAttributesModel(nodesIndex);
}

void CustomNodesView::onCustomContextMenu(const QPoint &point)
{
    const QModelIndex& index = indexAt(point).siblingAtColumn(0);

    const bool not_editable = !getNodesModel()->isEditable(index);
    treeContextMenu->setDisabled(not_editable);

    if(index.isValid())
    {
        NodesModel* model = getNodesModel();
        MemoryNode* node = model->getItem(index);

        add_child_submenu->clear();
        insert_before_sibling_submenu->clear();
        insert_after_sibling_submenu->clear();
        add_attribute_submenu->clear();
        remove_attribute_submenu->clear();
        python_actions_submenu->clear();

        for(const auto& child : node->getValidator()->getChildrenWithSpecialSequences())
        {
            QString child_name;
            if(child->getParent()->isSpecialSequence())
            {
                const std::vector<std::string> relative_name = child->getRelativeName(node->getValidator());
                child_name = QString::fromStdString(join(relative_name, "/"));
            }
            else
            {
                child_name = QString::fromStdString(child->getName());
            }
            QAction* add_child_action = new QAction(child_name, add_child_submenu);
            connect(add_child_action, &QAction::triggered, this, [this, child_name](){this->addChild(child_name);});
            add_child_submenu->addAction(add_child_action);
        }
        if(index.parent().isValid())
        {
            insert_before_sibling_submenu->setEnabled(true);
            insert_after_sibling_submenu->setEnabled(true);

            for(const auto& child : node->getParent()->getValidator()->getChildren())
            {
                if(child->isSpecialSequence() && node->getParent()->hasChild(child->getName()))
                    continue;
                const QString& child_name = QString::fromStdString(child->getName());

                QAction* add_child_before_action = new QAction(child_name, insert_before_sibling_submenu);
                connect(add_child_before_action, &QAction::triggered, this,
                        [this, child_name, index](){
                    this->addChild(child_name, index.parent(), index.row());
                });
                insert_before_sibling_submenu->addAction(add_child_before_action);

                QAction* add_child_after_action = new QAction(child_name, insert_after_sibling_submenu);
                connect(add_child_after_action, &QAction::triggered, this,
                        [this, child_name, index](){
                    this->addChild(child_name, index.parent(), index.row()+1);
                });
                insert_after_sibling_submenu->addAction(add_child_after_action);
            }
        }
        else
        {
            insert_before_sibling_submenu->setDisabled(true);
            insert_after_sibling_submenu->setDisabled(true);
        }
        // attributes list
        std::function<void(QMenu*, AttributeContainerValidator*)> fn;
        fn = [&fn, node, model, index, this](QMenu* add_menu, AttributeContainerValidator* validator)
        {
            for(const auto& attribute: validator->getAttributes())
            {
                const QString& full_attr_name = QString::fromStdString(join(attribute->getFullName(), "/"));
                if(!node->getAttributeContainer()->hasAttribute(attribute->getFullName()))
                {
                    QAction* add_attr_action = new QAction(QString::fromStdString(attribute->getName()), add_menu);
                    add_attr_action->setEnabled(attribute->isAddable());
                    connect(add_attr_action, &QAction::triggered, this, [this, full_attr_name](){this->addAttribute(full_attr_name);});
                    add_menu->addAction(add_attr_action);
                }
                else
                {
                    QAction* remove_attr_action = new QAction(full_attr_name, remove_attribute_submenu);
                    remove_attr_action->setEnabled(attribute->isRemovable());
                    Attribute* attr = node->getAttributeContainer()->getAttribute(attribute->getFullName());
                    const QModelIndex& attr_index = model->getAttributesModel(index)->getIndex(attr);
                    connect(remove_attr_action, &QAction::triggered, this, [this, attr_index, index]()
                    {
                        undoStack->push(new RemoveAttributeCommand(attr_index, index));
                    });
                    remove_attribute_submenu->addAction(remove_attr_action);
                }
            }
            for(const auto& container: validator->getAttributeContainers())
            {
                QMenu* container_menu = add_menu->addMenu(QString::fromStdString(container->getName()));
                fn(container_menu, container.get());
                container_menu->setDisabled(container_menu->isEmpty());
            }
        };

        AttributeContainerValidator* validator = node->getValidator()->getAttributes();
        fn(add_attribute_submenu, validator);

        // python actions
        auto python_actions = node->getValidator()->getPythonMenuActions();

        for(auto action: python_actions)
        {
            QAction* py_action = new QAction(QString::fromStdString(action.first), python_actions_submenu);
            connect(py_action, &QAction::triggered, this, [node, action]()
            {
                python_action(action.second.first, action.second.second, node);
            });
            python_actions_submenu->addAction(py_action);
        }

        const bool attribute_menu_empty = add_attribute_submenu->isEmpty();
        const bool child_menu_empty = add_child_submenu->isEmpty();
        const bool insert_before_sibling_submenu_empty = insert_before_sibling_submenu->isEmpty();
        const bool insert_after_sibling_submenu_empty = insert_after_sibling_submenu->isEmpty();
        const bool python_actions_submenu_empty = python_actions_submenu->isEmpty();

        add_attribute_submenu->setDisabled(attribute_menu_empty);
        add_child_submenu->setDisabled(child_menu_empty);
        add_submenu->setDisabled(attribute_menu_empty && child_menu_empty);
        remove_attribute_submenu->setDisabled(remove_attribute_submenu->isEmpty());
        insert_before_sibling_submenu->setDisabled(insert_before_sibling_submenu_empty);
        insert_after_sibling_submenu->setDisabled(insert_after_sibling_submenu_empty);
        python_actions_submenu->setDisabled(python_actions_submenu_empty);

        treeContextMenu->exec(viewport()->mapToGlobal(point));
    }
}

void CustomNodesView::addChild(const QString &child_type)
{
    addChild(child_type, currentIndex().siblingAtColumn(0));
}

void CustomNodesView::addChild(const QString &child_type, const int position)
{
    addChild(child_type, currentIndex().siblingAtColumn(0), position);
}

void CustomNodesView::addChild(const QString &child_type, const QModelIndex &parent_index)
{
    addChild(child_type, parent_index.siblingAtColumn(0), getNodesModel()->rowCount(parent_index));
}

void CustomNodesView::addChild(const QString &child_type, const QModelIndex &parent_index, const int position)
{
    NodesModel* model = getNodesModel();
    MemoryNode* parent_node = model->getItem(parent_index);

    // return if cannot edit
    if(!model->isEditable(parent_index))
        return;

    std::vector<std::string> types;
    split('/', types, child_type.toStdString());
    if(types.size() > 1)
    {
        undoStack->beginMacro("Added new " + child_type + " to " + QString::fromStdString(parent_node->getName()));
        undoStack->push(new AddChildrenCommand(types, parent_index.siblingAtColumn(0)));
        MemoryNode* last_child = parent_node->getChild(types);
        const QModelIndex& last_child_index = model->getIndex(last_child).siblingAtColumn(0);
        addRequiredAttributes(last_child_index);
        expand(last_child_index);
        scrollTo(last_child_index);
        undoStack->endMacro();
    }
    else
    {
        undoStack->beginMacro("Added new " + child_type + " to " + QString::fromStdString(parent_node->getName()));
        undoStack->push(new AddChildCommand(position, child_type, parent_index.siblingAtColumn(0)));
        MemoryNode* child = parent_node->getChildren().at(static_cast<size_t>(position)).get();
        const QModelIndex& child_index = model->getIndex(child).siblingAtColumn(0);
        addRequiredAttributes(child_index);
        undoStack->endMacro();
        // expand where inserted
        expand(parent_index);
        scrollTo(model->index(position, 0, parent_index));
    }
}

void CustomNodesView::addAttribute(const QString &attribute_name)
{
    addAttribute(attribute_name, currentIndex().siblingAtColumn(0));
}

void CustomNodesView::addAttribute(const QString &attribute_name, const QModelIndex &parent_index)
{
    // check if parent index is valid
    if(!parent_index.isValid())
        return;
    MemoryNode* node = getNodesModel()->getItem(parent_index);

    // return if cannot edit
    if(!getNodesModel()->isEditable(parent_index))
        return;

    // check if the attribute is already there
    std::vector<std::string> attr_name_vec;
    split('/', attr_name_vec, attribute_name.toStdString());

    std::vector<std::string> container_names;
    const std::string attr_name = *(attr_name_vec.end()-1);
    if(attr_name_vec.size() > 1)
            container_names = std::vector<std::string>(attr_name_vec.begin(), attr_name_vec.end()-1);
    if(!node->getAttributeContainer()->hasAttribute(attr_name_vec))
    {
        undoStack->beginMacro("Added attribute " + attribute_name + " to " + QString::fromStdString(node->getName()));
        AttributeContainer* parent_container = node->getAttributeContainer()->getAttributeContainer(container_names);
        AttributesModel * model = getAttributesModel(parent_index);
        if(!parent_container)
        {
            parent_container = node->getAttributeContainer();
            for(const auto& container_name : container_names)
            {
                AttributeContainer* child_container = parent_container->getAttributeContainer(container_name);
                if(!child_container)
                {
                    const QModelIndex& container_index = model->getIndex(parent_container);
                    undoStack->push(new AddAttributeContainerCommand(model->rowCount(container_index), QString::fromStdString(container_name), parent_index, container_index, model));
                    parent_container = parent_container->getAttributeContainer(container_name);
                }
                else
                {
                    parent_container = child_container;
                }
            }
        }
        const QModelIndex& container_index = model->getIndex(parent_container);

        AttributeValidator* attr_validator = parent_container->getValidator()->getAttributeValidator(attr_name);

        QString default_value;
        if(attr_validator->hasDynamicDefaultValue())
        {
            default_value = QString::fromStdString(attr_validator->getDynamicDefaultValue());
        }
        else
        {
            default_value = QString::fromStdString(attr_validator->getDefaultValue());
        }


        if(default_value.isEmpty())
        {
            // for bool, get false as the default
            const std::string& attr_type = attr_validator->getAttributeType();
            if(attr_type == "bool")
            {
               default_value = "false";
            }
            // if it has enum take the first value
            const NodeEnumValidator * enum_validator = nullptr;
            enum_validator = attr_validator->getValidator(enum_validator);
            if(enum_validator)
            {
                if(enum_validator->getEnums().size())
                {
                    default_value = QString::fromStdString(enum_validator->getEnums().front());
                }
            }
            // no default value for py enums, since Attribute needs to be present...
            // @TODO figure something out...
        }
        undoStack->push(new AddAttributeCommand(
                                                QString::fromStdString(attr_name),
                                                parent_index,
                                                container_index,
                                                model,
                                                default_value));
        undoStack->endMacro();
    }
}

void CustomNodesView::addRequiredAttributes(const QModelIndex &parent_index)
{
    MemoryNode* node = getNodesModel()->getItem(parent_index);
    for(const auto& attribute: node->getAttributeContainer()->getValidator()->requiredAttributes())
    {
        addAttribute(QString::fromStdString(join(attribute->getFullName(), "/")), parent_index);
    }
}

void CustomNodesView::duplicateNodes()
{
    if(!model())
        return;
    const auto& indexes = getOnlyColumn0(selectionModel()->selectedIndexes());
    if(indexes.empty())
        return;
    if(!checkIfSameParent(indexes))
        return;
    if(!indexes.front().parent().isValid()) // do not duplicate root element
        return;

    // return if cannot edit
    if(!getNodesModel()->isEditable(indexes.front()))
        return;

    const bool multiple = indexes.size() > 1;

    if(multiple)
        undoStack->beginMacro("Duplicated multiple nodes");

    const int offset = indexes.size();
    for(const auto& index: indexes)
    {
        MemoryNode* node = getNodesModel()->getItem(index);
        if(node->isSpecialSequence()) // do not duplicate special sequences
            continue;
        undoStack->push(new DuplicateNodeCommand(index, index.row()+offset, index.parent()));
    }
    if(multiple)
        undoStack->endMacro();
}

void CustomNodesView::removeNodes()
{
    if(!model())
        return;
    QModelIndexList indexes = getOnlyColumn0(selectionModel()->selectedIndexes());
    QPersistentModelIndex lower_sibling, parent;

    for(const auto& index: indexes)
    {
        if(!index.parent().isValid())
            return; // do not remove root element
        // return if cannot edit
        if(!getNodesModel()->isEditable(index))
            return;
    }

    // do a macro if more than 1
    const bool multiple = indexes.size() > 1;
    if(multiple)
        undoStack->beginMacro("Removed multiple nodes");

    while(indexes.size())
    {
        const QModelIndex& index = indexes[0];
        lower_sibling = index.siblingAtRow(index.row()+1);
        parent = index.parent();
        undoStack->push(new RemoveNodeCommand(index));
        // quite important, since iterators (indexes) are invalidated after an item is removed
        indexes = getOnlyColumn0(selectionModel()->selectedIndexes());
    }

    // close macro
    if(multiple)
        undoStack->endMacro();

    if(lower_sibling.isValid())
    {
        selectionModel()->select(lower_sibling, QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect);
        selectionModel()->setCurrentIndex(lower_sibling, QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect);
    }
    else if (parent.isValid())
    {
        selectionModel()->select(parent, QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect);
        selectionModel()->setCurrentIndex(parent, QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect);
    }
    else
    {
        auto idx = model()->index(0, 0, rootIndex());
        selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect);
        selectionModel()->setCurrentIndex(idx, QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect);
    }
}

void CustomNodesView::moveUpNode()
{
    moveNodes(MoveCommand::up);
}

void CustomNodesView::moveDownNode()
{
    moveNodes(MoveCommand::down);
}

void CustomNodesView::copyOrCut(const QModelIndexList &indexes, const Qt::DropAction& action)
{
    QMimeData* data = getNodesModel()->mimeData(indexes, action);
    QClipboard* clipboard = QApplication::clipboard();
    clipboard->setMimeData(data);
}

void CustomNodesView::moveNodes(MoveCommand::MoveDirection dir)
{
    if(!model())
        return;

    const QModelIndexList& sel_indexes = selectedIndexes();
    if(sel_indexes.empty())
        return;
    // check if they have the same parent
    if(!checkIfSameParent(sel_indexes))
        return;
    // return if cannot edit
    if(!getNodesModel()->isEditable(sel_indexes.front()))
        return;
    undoStack->push(new MoveCommand(dir, sel_indexes.first().siblingAtColumn(0), sel_indexes.size()/2));
}

void CustomNodesView::populateModel(std::unique_ptr<MemoryNode> root, ValidatorNode* validator)
{
    // fake root, one level more for the model
    std::unique_ptr<MemoryNode> fake_root = std::make_unique<MemoryNode>();
    fake_root->addChild(std::move(root));
    fake_root->setValidator(validator);

    // tree model
    NodesModel * model = new NodesModel(std::move(fake_root), undoStack, this);
    setModel(model);
    resizeColumnToContents(0);
    Q_EMIT modelChanged();
}

void CustomNodesView::clear()
{
    getNodesModel()->deleteLater();
    setModel(nullptr);
}
